package com.autumn.platform.web.argumentresolver;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;

import com.autumn.platform.core.logger.Logs;

public class BodyAndModelHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private final List<MediaType> jsonMediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));

    private final LocalRequestResponseBodyMethodProcessor body;

    private final LocalServletModelAttributeMethodProcessor model = new LocalServletModelAttributeMethodProcessor();

    public BodyAndModelHandlerMethodArgumentResolver(List<HttpMessageConverter<?>> converters, List<Object> requestResponseBodyAdvice) {
        this.body = new LocalRequestResponseBodyMethodProcessor(converters, requestResponseBodyAdvice);
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return model.supportsParameter(parameter);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object arg = this.resolveBodyArgument(parameter, mavContainer, webRequest, binderFactory);
        arg = this.resolveModelArgument(arg, parameter, mavContainer, webRequest, binderFactory);
        return arg;
    }

    private boolean isJsonContentType(MediaType type) {
        for (MediaType m : this.jsonMediaTypes) {
            if (m.includes(type)) {
                return true;
            }
        }
        return false;
    }

    private static InputStream getBodyFromQueryString(String s, Charset charset) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(bos, charset);
        writer.write(URLDecoder.decode(s, charset.name()));
        writer.flush();
        return new ByteArrayInputStream(bos.toByteArray());
    }

    private Object resolveBodyArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        try {
            if (null != body) {
                parameter = parameter.nestedIfOptional();
                Object arg = body.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
                return arg;
            }
        } catch (Exception ignore) {
            Logs.debug(ignore.getMessage());
        }
        return null;
    }

    private Object resolveModelArgument(Object arg, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String name = ModelFactory.getNameForParameter(parameter);
        Object attribute = null;
        if (mavContainer.containsAttribute(name)) {
            attribute = mavContainer.getModel().get(name);
        } else {
            attribute = model.createAttributeLocal(arg, name, parameter, binderFactory, webRequest);
        }

        if (!mavContainer.isBindingDisabled(name)) {
            ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
            if (ann != null && !ann.binding()) {
                mavContainer.setBindingDisabled(name);
            }
        }

        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        if (binder.getTarget() != null) {
            if (!mavContainer.isBindingDisabled(name)) {
                model.bindRequestParameters(binder, webRequest);
            }
            model.validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && model.isBindExceptionRequired(binder, parameter)) {
                throw new BindException(binder.getBindingResult());
            }
        }

        // Add resolved attribute and BindingResult at the end of the model
        Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);

        return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }

    private class LocalRequestResponseBodyMethodProcessor extends RequestResponseBodyMethodProcessor {

        public LocalRequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters, List<Object> requestResponseBodyAdvice) {
            super(converters, requestResponseBodyAdvice);
        }

        @Override
        public <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
            final HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
            final HttpMethod method = HttpMethod.resolve(servletRequest.getMethod());
            ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest) {
                @Override
                public InputStream getBody() throws IOException {
                    if (HttpMethod.GET.equals(method) || HttpMethod.DELETE.equals(method)) {
                        Charset charset = getHeaders().getContentType().getCharset();
                        if (null == charset) {
                            charset = FORM_CHARSET;
                        }
                        String s = servletRequest.getQueryString();
                        return getBodyFromQueryString(s, charset);
                    } else {
                        return super.getBody();
                    }
                }
            };
            MediaType type = inputMessage.getHeaders().getContentType();
            if (isJsonContentType(type)) {
                Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
                if (arg == null) {
                    if (checkRequired(parameter)) {
                        throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getMethod().toGenericString());
                    }
                }
                return arg;
            } else {
                return null;
            }
        }
    }

    private class LocalServletModelAttributeMethodProcessor extends ServletModelAttributeMethodProcessor {

        public LocalServletModelAttributeMethodProcessor() {
            super(true);
        }

        private Object createAttributeLocal(Object arg, String attributeName, MethodParameter methodParam, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
            if (null != arg) {
                return arg;
            }
            return super.createAttribute(attributeName, methodParam, binderFactory, request);
        }

        @Override
        protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
            super.bindRequestParameters(binder, request);
        }

        @Override
        protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
            super.validateIfApplicable(binder, methodParam);
        }

        @Override
        protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
            return super.isBindExceptionRequired(binder, methodParam);
        }
    }
}
